#pragma once

#ifdef ZENO_HAS_CPP20_FORMAT
#include <format>
#include <iterator>
#include <algorithm>
#define ZENO_MINILOG_FMT_NAMESPACE std
#else
#include <fmt/format.h>
#define ZENO_MINILOG_FMT_NAMESPACE fmt
#endif
#include <source_location>
#include <chrono>
#include <utility>
#include <sstream>
#include <stdexcept>
#include <concepts>
#include <zenox/postprocess.hpp>
#include <zenox/pragma.hpp>

namespace zenox {

inline namespace minilog {

#ifdef ZENO_HAS_CPP20_FORMAT
#define ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION details::with_source_location<std::format_string<Args...>>
#else
#define ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION details::with_source_location<std::string_view>
#endif

// https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
#define ZENO_MINILOG_FOREACH_LOG_LEVEL(f) \
    f(trace) \
    f(debug) \
    f(info) \
    f(critical) \
    f(warn) \
    f(error) \
    f(fatal)

enum class log_level : unsigned char {
#define _FUNCTION(name) name,
    ZENO_MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION
};

namespace details {

struct stream_log_tag {
    explicit stream_log_tag() = default;
};

template <class T>
struct with_source_location {
private:
    T inner;
    std::source_location loc;

public:
    template <class U> requires std::constructible_from<T, U>
    consteval with_source_location(U &&inner, std::source_location loc = std::source_location::current())
    : inner(std::forward<U>(inner)), loc(std::move(loc)) {}
    constexpr std::string_view format() const { return inner.get(); }
    constexpr std::source_location const &location() const { return loc; }
};

template <>
struct with_source_location<std::string_view> {
private:
    std::string_view inner;
    std::source_location loc;

public:
    template <class U> requires std::constructible_from<std::string_view, U>
    constexpr with_source_location(U &&inner, std::source_location loc = std::source_location::current())
    : inner(std::forward<U>(inner)), loc(std::move(loc)) {}
    constexpr std::string_view format() const { return inner; }
    constexpr std::source_location const &location() const { return loc; }
};

struct fatal_exception : std::exception {
    std::string m_msg;
    fatal_exception(std::string msg) noexcept;
    const char *what() const noexcept override;
    ~fatal_exception() noexcept;
};

[[noreturn]] ZENO_NORETURN ZENO_COLD_FN void throw_fatal_exception(std::string_view msg, std::source_location const &loc);

struct log_stream_adapter;

}

struct logger {
private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;

public:
    logger();
    ~logger();

    static ZENO_CONST_FN logger &default_logger();

    void set_log_file(std::string path, bool override_environment_variable = true);
    void set_log_level(log_level lev);
    void flush_log();

    void output_log(log_level lev, std::string_view msg, std::source_location const &loc = std::source_location::current());

    template <typename... Args>
    void format_log(log_level lev, ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION fmt, Args &&...args) {
        auto const &loc = fmt.location();
        auto msg = ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt.format(), ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
        output_log(lev, msg, loc);
    }

    template <log_level lev, typename... Args>
    void format_log(ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION fmt, Args &&...args) {
#ifdef NDEBUG
        if constexpr (lev > log_level::trace) {
#endif
            auto const &loc = fmt.location();
            auto msg = ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt.format(), ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
            output_log(lev, msg, loc);
#ifdef NDEBUG
        } else {
            ((void)fmt, ..., (void)args);
        }
#endif
    }

    struct log_stream_adapter {
        std::ostringstream oss;
        logger &lg;
        log_level lev;
        bool had_once = false;
        std::source_location loc;

        explicit log_stream_adapter(logger &lg, log_level lev, std::source_location const &loc) : lg(lg), lev(lev), loc(loc) {}

        log_stream_adapter(log_stream_adapter &&) = delete;

        template <class T>
        log_stream_adapter &operator<<(T &&t) { // use this if you have ostream << operator overload
            if (had_once)
                oss << ' ';
            oss << t;
            had_once = true;
            return *this;
        }

        template <class T>
        log_stream_adapter &operator<=(T &&t) { // this if you have fmt::formatter specialization
            if (had_once)
                oss << ' ';
            oss << ZENO_MINILOG_FMT_NAMESPACE::format("{}", t);
            had_once = true;
            return *this;
        }

        ~log_stream_adapter() {
            auto msg = oss.str();
            if (!msg.empty()) [[likely]] {
                if (msg.back() == '\n') {
                    msg.pop_back();
                }
                lg.output_log(lev, msg, loc);
            }
        }
    };

    log_stream_adapter stream_log(log_level lev, std::source_location const &loc = std::source_location::current()) {
        return log_stream_adapter{logger::default_logger(), lev, loc};
    }
};

#ifdef ZENO_MINILOG_DISABLE
#define _FUNCTION(name) \
template <typename... Args> \
ZENO_FORCEINLINE void log_##name(Args &&...) { \
}
#else
#define _FUNCTION(name) \
template <typename... Args> \
void log_##name(ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION fmt, Args &&...args) { \
    return logger::default_logger().format_log<log_level::name>(std::move(fmt), std::forward<Args>(args)...); \
}
#endif
ZENO_MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION

#define _FUNCTION(name) \
inline logger::log_stream_adapter log_##name(details::stream_log_tag = details::stream_log_tag{}, std::source_location const &loc = std::source_location::current()) { \
    return logger::default_logger().stream_log(log_level::name, loc); \
}
ZENO_MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION

#ifdef ZENO_HAS_CPP20_FORMAT
template <typename... Args>
constexpr std::string static_format(ZENO_MINILOG_FMT_NAMESPACE::format_string<Args...> fmt, Args &&...args) {
    return ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt.get(), ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
}
#else
template <typename... Args>
constexpr std::string static_format(std::string_view fmt, Args &&...args) {
    return ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt, ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
}
#endif

template <typename... Args>
constexpr std::string format(std::string_view fmt, Args &&...args) {
    return ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt, ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
}

template <typename... Args>
void format_log(log_level lev, ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION fmt, Args &&...args) {
    return logger::default_logger().format_log(lev, std::move(fmt), std::forward<Args>(args)...);
}

template <typename... Args>
[[noreturn]] ZENO_NORETURN ZENO_COLD_FN void fatal_error(ZENO_MINILOG_DETAILS_WITH_SOURCE_LOCATION fmt, Args &&...args) {
    // fatal_error is used when reporting unrecoverable errors
    auto const &loc = fmt.location();
    auto msg = ZENO_MINILOG_FMT_NAMESPACE::vformat(fmt.format(), ZENO_MINILOG_FMT_NAMESPACE::make_format_args(args...));
    details::throw_fatal_exception(std::move(msg), loc);
}

ZENO_FORCEINLINE static auto _zeno_assert_s_impl(auto t) {
    bool cond = (bool)t;
    ZENO_ASSUME(cond);
    return t;
}
ZENO_FORCEINLINE static auto _zeno_assert_s_impl(auto t, auto &&pred) {
    ZENO_ASSUME(pred(t));
    return t;
}
ZENO_FORCEINLINE static auto _zeno_assert_f_impl(auto t, const char *expr, std::source_location const &loc = std::source_location::current()) {
    bool cond = (bool)t;
    if (!cond) [[unlikely]] {
        details::throw_fatal_exception(ZENO_MINILOG_FMT_NAMESPACE::format("ZENO_ASSERT failed: {}", expr), loc);
    }
    return t;
}
ZENO_FORCEINLINE static auto _zeno_assert_f_impl(auto t, const char *expr, auto &&pred, std::source_location const &loc = std::source_location::current()) {
    bool cond = pred(t);
    if (!cond) [[unlikely]] {
        details::throw_fatal_exception(ZENO_MINILOG_FMT_NAMESPACE::format("ZENO_ASSERT failed: {}", expr), loc);
    }
    return t;
}
#define ZENO_ASSERT_S_1(x) (::zenox::minilog::_zeno_assert_s_impl((x)))
#define ZENO_ASSERT_S_2(x, test) (::zenox::minilog::_zeno_assert_s_impl((x), [&] (auto const &_) { return (bool)(test); }))
#define ZENO_ASSERT_S_3(x, test, msg) ZENO_ASSERT_S_2(x, test)
#define ZENO_ASSERT_S(...) ZENO_PP_CALL(ZENO_PP_CONCAT(ZENO_ASSERT_S_, ZENO_PP_COUNT_ARGS(__VA_ARGS__)), __VA_ARGS__)
#define ZENO_ASSERT_F_1(x) (::zenox::minilog::_zeno_assert_f_impl((x), #x))
#define ZENO_ASSERT_F_2(x, test) (::zenox::minilog::_zeno_assert_f_impl((x), #test ", where _ = " #x, [&] (auto &&_) { return (bool)(test); }))
#define ZENO_ASSERT_F_3(x, test, msg) (::zenox::minilog::_zeno_assert_f_impl((x), (msg), [&] (auto const &_) { return (bool)(test); }))
#define ZENO_ASSERT_F(...) ZENO_PP_CALL(ZENO_PP_CONCAT(ZENO_ASSERT_F_, ZENO_PP_COUNT_ARGS(__VA_ARGS__)), __VA_ARGS__)
#ifndef NDEBUG
#define ZENO_ASSERT(...) ZENO_ASSERT_F(__VA_ARGS__)
#else
#define ZENO_ASSERT(...) ZENO_ASSERT_S(__VA_ARGS__)
#endif
#define ZENO_ASSERT_R(x, ...) do { if (!(x)) [[unlikely]] {zenox::log_debug("ZENO_ASSERT_R triggered: " #x);  return __VA_ARGS__; } } while (0)

#ifdef ZENO_HAS_CPP20_FORMAT
namespace details {

template <typename It, typename Sentinel, typename Char = char>
struct join_view {
    It begin;
    Sentinel end;
    std::basic_string_view<Char> sep;

    join_view(It b, Sentinel e, std::basic_string_view<Char> s)
    : begin(b), end(e), sep(s) {}
};

}
}
}

template <typename It, typename Sentinel, typename Char>
struct ZENO_MINILOG_FMT_NAMESPACE::formatter<zenox::minilog::details::join_view<It, Sentinel, Char>, Char> {
private:
    using value_type = typename std::iterator_traits<It>::value_type;
    formatter<std::remove_cvref_t<value_type>, Char> value_formatter_;

public:
    template <typename ParseContext>
    constexpr auto parse(ParseContext& ctx) -> const Char* {
        return value_formatter_.parse(ctx);
    }

    template <typename FormatContext>
    auto format(const ::zenox::minilog::details::join_view<It, Sentinel, Char>& value,
                FormatContext& ctx) const -> decltype(ctx.out()) {
        auto it = value.begin;
        auto out = ctx.out();
        if (it != value.end) {
            out = value_formatter_.format(*it, ctx);
            ++it;
            while (it != value.end) {
                out = std::copy(value.sep.begin(), value.sep.end(), out);
                ctx.advance_to(out);
                out = value_formatter_.format(*it, ctx);
                ++it;
            }
        }
        return out;
    }
};

namespace zenox {
inline namespace minilog {

template <class It, class Sentinel, class Sep>
static details::join_view<It, Sentinel> join(It it, Sentinel eit, Sep &&sep) {
    return {it, eit, std::forward<Sep>(sep)};
}

template <class Range, class Sep>
static auto join(Range &&range, Sep &&sep) {
    return join(std::begin(range), std::end(range), std::forward<Sep>(sep));
}
#else
using ZENO_MINILOG_FMT_NAMESPACE::join;
#endif

template <class T>
concept has_to_string_method = requires (T const &t) {
    { t.to_string() } -> std::convertible_to<std::string_view>;
};

}

}

// so just add a member `std::string to_string() const;` to your class, to make it compatible with zenox::format
template <zenox::minilog::has_to_string_method T>
struct ZENO_MINILOG_FMT_NAMESPACE::formatter<T> {
    formatter<std::string_view> m_string;

public:
    constexpr auto parse(format_parse_context &ctx) {
        return m_string.parse(ctx);
    }

    constexpr auto format(T const &t, format_context &ctx) const {
        std::string_view s = t.to_string();
        m_string.format(s, ctx);
    }
};
